﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using XOS_VHD_Tools.VHD;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Collections;

namespace XOS_VHD_FAT32
{
    class Directory : FSItem, IDirectory
    {
        private readonly bool _isRoot;

        public Directory(DirEntry entry, FAT32Driver drv, bool isRoot = false)
            : base(entry, drv)
        {
            _isRoot = isRoot;

            if (!entry.Attrib.HasFlag(DirEntryAttrib.Directory))
            {
                throw new ArgumentOutOfRangeException("entry");
            }

            // let's clear this directory
            new FATCleaner(_drv).CleanDirectory(this);
        }

        public IEnumerable<IDirectory> Directories
        {
            get { return Entries.OfType<IDirectory>(); }
        }

        public IEnumerable<IFile> Files
        {
            get { return Entries.OfType<IFile>(); }
        }

        public bool IsRootDirectory { get { return _isRoot; } }

        internal Stream GetDirectoryStream()
        {
            var entr = _entry;
            if (entr.FirstCluster >= 2 && entr.FirstCluster < 0x0ffffff0)
            {
                entr.Attrib &= ~DirEntryAttrib.Directory;
                entr.Size = GetDirSize();

                Trace.WriteLine("Directory " + entr.Name.Trim() + " takes " + entr.Size + " bytes (" + (entr.Size / 32) + " entry(-s))");

                return new File(entr, _drv).OpenFile();
            }

            throw new InvalidOperationException();
        }

        internal IEnumerable<byte[]> EnumerateDirEntries()
        {
            var fDir = GetDirectoryStream();
            if (fDir != null)
            {
                using (fDir)
                {
                    var buff = new byte[32];

                    while (fDir.Read(buff, 0, buff.Length) == 32)
                    {
                        yield return buff;
                    }
                }
            }
        }

        private struct entry
        {
            public DirEntry Entry;
            public string LFN;
            public int EntryIndex;
        }

        private readonly List<entry> _entries = new List<entry>();

        private void UpdateEntriesTable()
        {
            _entries.Clear();

            int q = -1;

            var lfn = new LFNBuilder();

            foreach (var buff in EnumerateDirEntries())
            {
                q++;

                // check if we have and LFN record
                var c = buff[0];

                if (c == '.')
                {
                    lfn.Clear();
                    Trace.WriteLine(" entry " + q + " is [.], [..] or damaged record");
                    continue;
                }

                if (c == 0x05 || c == 0xe5)
                {
                    // File is deleted - let's skip it
                    lfn.Clear();
                    Trace.WriteLine(" entry " + q + " is deleted");
                    continue;
                }

                if (buff[0x0b] == 0x0f &&
                    buff[0x0c] == 0x00 &&
                    BitConverter.ToInt16(buff, 0x1A) == 0x00)
                {
                    // LFN entry
                    var lfnObj = buff.ConvertTo<LFN>();

                    if (lfnObj.IsValid)
                    {
                        lfn.Append(lfnObj);
                        continue;
                    }
                }

                if (c == '\0')
                {
                    lfn.Clear();
                    continue;
                }


                if (c <= ' ' || c > 'Z' && !(c >= 'a' && c <= 'z'))
                {
                    lfn.Clear();

                    Trace.WriteLineIf(c != 0, " entry " + q + " is corrupted. First byte can't be eq 0x" + c.ToString("X2"));
                    continue;
                }

                var dirEntry = buff.ConvertTo<DirEntry>();

                if (dirEntry.Attrib == (DirEntryAttrib.ReadOnly |
                                        DirEntryAttrib.Hidden |
                                        DirEntryAttrib.System |
                                        DirEntryAttrib.VolumeLabel))
                {
                    lfn.Clear();
                    Trace.WriteLine(" entry " + q + " is VolumeInfo file");
                    continue;
                }

                if (dirEntry.Attrib.HasFlag(DirEntryAttrib.VolumeLabel))
                {
                    if (_isRoot)
                    {
                        if (lfn.ValidateDirEntry(dirEntry))
                        {
                            _name = lfn.Name;
                        }
                        else
                        {
                            _name = dirEntry.Name.Trim();
                        }
                    }

                    lfn.Clear();

                    continue;
                }


                if (dirEntry.Attrib.HasFlag(DirEntryAttrib.Directory))
                {
                    Trace.WriteLine(" entry " + q + " is directory: " + dirEntry.Name.Trim());
                }
                else
                {
                    Trace.WriteLine(" entry " + q + " is file: " + dirEntry.Name.ToString());
                }

                var ent = new entry
                {
                    Entry = dirEntry,
                    EntryIndex = q,
                };

                if (lfn.ValidateDirEntry(dirEntry))
                {
                    ent.LFN = lfn.Name;
                }

                _entries.Add(ent);
                lfn.Clear();
            }
        }

        public IEnumerable<IFSEntry> Entries
        {
            get
            {
                if (_entries.Count == 0)
                {
                    UpdateEntriesTable();
                }

                foreach (var entry in _entries)
                {
                    var dirEntry = entry.Entry;

                    if (dirEntry.Attrib.HasFlag(DirEntryAttrib.Directory))
                    {
                        var dir = new Directory(dirEntry, _drv) { ParentDirectory = this };

                        if (!string.IsNullOrWhiteSpace(entry.LFN))
                        {
                            dir.Name = entry.LFN;
                        }

                        yield return dir;
                    }
                    else
                    {
                        var file = new File(dirEntry, _drv) { ParentDirectory = this };

                        if (!string.IsNullOrWhiteSpace(entry.LFN))
                        {
                            file.Name = entry.LFN;
                        }

                        yield return file;
                    }
                }
            }
        }

        private uint GetDirSize()
        {
            var c = _entry.FirstCluster;

            var i = 0;

            while (c >= 2 && c < 0x0ffffff0 && i < _drv._fatChain.Length)
            {
                i++;

                c = _drv._fatChain[c];
            }

            return (uint)(i * _drv._bytesPerSect * _drv._sectPerClust);
        }

        public override string ToString()
        {
            return Name + "/";
        }

        private DirEntry CreateEntry(string name, DirEntryAttrib attributes)
        {
            // Is filename has some information?
            if (string.IsNullOrWhiteSpace(name))
            {
                throw new ArgumentNullException();
            }

            name = name.Trim();

            // Is filename short enough?
            if (name.Length > 200)
            {
                throw new PathTooLongException();
            }

            // Do we have a valid file name?
            if (name.IndexOfAny(Path.GetInvalidFileNameChars()) != -1)
            {
                throw new IOException();
            }

            // Do we have an entry with the same name?
            if (Entries.Any(t => t.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase)))
            {
                throw new IOException();
            }

            // Do we have some place for new file?
            uint i = 2;
            for (; i < _drv._fatChain.Length && _drv._fatChain[i] != 0; i++)
            { }

            if (i < _drv._fatChain.Length)
            {
                // empty FAT entry exists
                _drv._fatChain[i] = 0xffffffff;

                _drv.FlushFAT();
            }
            else
            {
                // Disk is full (??)
                throw new InsufficientMemoryException();
            }

            var shortName = Get83Name(name);


            // Let's create main entry
            var entry = new DirEntry
            {
                Name = shortName,
                Size = 0,
                Attrib = attributes,
                FirstCluster = i
            };

            // generating LFN entries (DirEntries)
            var lfnEntries = LFN.GetLFNEntries(name, shortName).ToArray();

            // let's find some slot for 


            int q = 0;
            var index = -1;
            var offset = -1;
            var compressionIsMade = false;

        tryFindSlot:
            foreach (var buff in EnumerateDirEntries())
            {
                index++;

                var c = buff[0];

                if (c == 0 || c == 0xe5)
                {
                    // empty or deleted record
                    q++;
                    if (q >= lfnEntries.Length + 1)
                    {
                        // ok, we've found the place
                        // offset = index * (lfnEntries.Length + 1) * 32
                        offset = index * (lfnEntries.Length + 1) * 32;
                        break;
                    }
                    continue;
                }

                q = 0;
            }

            if (offset == -1)
            {
                if (!compressionIsMade)
                {
                    // Compressing folder
                    new FATCleaner(_drv).CleanDirectory(this);

                    compressionIsMade = true;

                    goto tryFindSlot;
                }

                // if compression is not useful - let's enlarge directory file
            }

            using (var dir = this.GetDirectoryStream())
            {
                if (offset == -1)
                {
                    offset = int.MaxValue;
                }
                else
                {
                    offset -= (lfnEntries.Length + 1) * 32;
                }

                dir.Position = Math.Min(offset, dir.Length);

                var dirEntBytes = lfnEntries.Select(t => t.ToByteArray()).SelectMany(t => t).Concat(entry.ToByteArray()).ToArray();
                dir.Write(dirEntBytes, 0, dirEntBytes.Length);
            }

            _entries.Clear();

            return entry;
        }



        private string Get83Name(string name)
        {
            var useTilda = false;

            var ext = Path.GetExtension(name);

            if (ext.Length > 0)
            {
                ext = ext.Substring(1);

                if (ext.Length > 3)
                {
                    ext = ext.Substring(0, 3);
                    useTilda = true;
                }
            }

            name = Path.GetFileNameWithoutExtension(name);

            if (name.Length > 8)
            {
                name = name.Substring(0, 8);
                useTilda = true;
            }

            var dotId = name.IndexOf('.');
            if (dotId != -1)
            {
                name = name.Substring(0, dotId);
                useTilda = true;
            }


        changeFileName:
            if (useTilda)
            {
                var id = 0;
                const int limit = 1000;

                var pLen = 1;
                for (; id < limit - 1; id++)
                {
                    var sid = id.ToString();

                    name = name.Substring(0, 8 - pLen - 1) + "~" + sid;


                    if (!Entries.Cast<FSItem>().Any(t => t.ShortName.Equals(name, StringComparison.CurrentCultureIgnoreCase)))
                    {
                        // Ok, this name is not reserved - let's use it
                        break;
                    }

                    pLen = sid.Length;
                }

                if (id == limit)
                {
                    // we can't create file
                    throw new FileNotFoundException();
                }
            }
            else if (Entries.Cast<FSItem>().Any(t => t.ShortName.Equals(name, StringComparison.CurrentCultureIgnoreCase)))
            {
                useTilda = true;
                goto changeFileName;
            }

            return (ext.Length == 0 ? name : name + "." + ext).ToUpper();
        }

        public IDirectory CreateDirectory(string name)
        {
            var dir = Directories.FirstOrDefault(t => string.Compare(t.Name, name, true) == 0);

            if (dir != null)
            {
                return dir;
            }

            var entry = CreateEntry(name, DirEntryAttrib.Directory);

            try
            {
                return new Directory(entry, _drv);
            }
            finally
            {
                OnPropertyChanged("Entries");
            }
        }

        public IFile CreateFile(string name)
        {
            var file = Files.FirstOrDefault(t => string.Compare(t.Name, name, true) == 0);

            if (file != null)
            {
                return file;
            }

            var entry = CreateEntry(name, DirEntryAttrib.None);

            try
            {
                return new File(entry, _drv);
            }
            finally
            {
                OnPropertyChanged("Entries");
            }
        }

        private uint[] GetClusterChain(uint clust)
        {
            var items = new List<uint>();

            while (clust < 0x0ffffff0)
            {
                items.Add(clust);
                clust = _drv.GetNextClusterID(clust);
            }

            return items.ToArray();
        }

        public void DeleteItem(IFSEntry obj)
        {
            if (obj != null)
            {
                foreach (var entry in Entries)
                {
                    if (entry.Name.Equals(obj.Name, StringComparison.CurrentCultureIgnoreCase))
                    {
                        var dirEnt = (entry as FSItem).FAT32DirEntry.ToByteArray();

                        var i = 0;
                        foreach (var de in EnumerateDirEntries())
                        {
                            if (de.SequenceEqual(dirEnt))
                            {
                                break;
                            }

                            i += dirEnt.Length;
                        }

                        if (entry is IDirectory)
                        {
                            var dir = entry as IDirectory;
                            foreach (var subItem in dir.Entries)
                            {
                                dir.DeleteItem(subItem);
                            }
                        }

                        // item's offset is i

                        using (var stream = GetDirectoryStream())
                        {
                            stream.Position = i;
                            stream.Write(new byte[32], 0, 32);     // no file

                            var chain = GetClusterChain((entry as FSItem).FAT32DirEntry.FirstCluster);

                            foreach (var clust in chain)
                            {
                                _drv._fatChain[clust] = 0;
                            }
                        }

                        _drv.FlushFAT();

                        _entries.Clear();
                        OnPropertyChanged("Entries");

                        break;
                    }
                }
            }
        }

        public void RenameItem(IFSEntry entry, string name)
        {
            throw new NotImplementedException();
        }

    }

}
